14.3 堵塞

一个线程可以有四种状态:

(1) 新(New):线程对象已经创建,但尚未启动,所以不可运行。

(2) 可运行(Runnable):意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那个线程便可立即开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既没有“死”掉,也未被“堵塞”。

(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会产生一个异常——属于Error的一个子类(也就是说,我们通常不捕获它)。记住一个异常的“抛”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2则是坚决反对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。

(4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地跳过它,不给它分配任何CPU时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。

14.3.1 为何会堵塞

堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造成的:

(1) 调用sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。

(2) 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回“可运行”状态。

(3) 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成“可运行”(是的,这看起来同原因2非常相象,但有一个明显的区别是我们马上要揭示的)。

(4) 线程正在等候一些IO(输入输出)操作完成。

(5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。

亦可调用yield()Thread类的一个方法)自动放弃CPU,以便其他线程能够运行。然而,假如调度机制觉得我们的线程已拥有足够的时间,并跳转到另一个线程,就会发生同样的事情。也就是说,没有什么能防止调度机制重新启动我们的线程。线程被堵塞后,便有一些原因造成它不能继续运行。

下面这个例子展示了进入堵塞状态的全部五种途径。它们全都存在于名为Blocking.java的一个文件中,但在这儿采用散落的片断进行解释(大家可注意到片断前后的Continued以及Continuing标志。利用第17章介绍的工具,可将这些片断连结到一起)。首先让我们看看基本的框架:

  1. //: Blocking.java
  2. // Demonstrates the various ways a thread
  3. // can be blocked.
  4. import java.awt.*;
  5. import java.awt.event.*;
  6. import java.applet.*;
  7. import java.io.*;
  8. //////////// The basic framework ///////////
  9. class Blockable extends Thread {
  10. private Peeker peeker;
  11. protected TextField state = new TextField(40);
  12. protected int i;
  13. public Blockable(Container c) {
  14. c.add(state);
  15. peeker = new Peeker(this, c);
  16. }
  17. public synchronized int read() { return i; }
  18. protected synchronized void update() {
  19. state.setText(getClass().getName()
  20. + " state: i = " + i);
  21. }
  22. public void stopPeeker() {
  23. // peeker.stop(); Deprecated in Java 1.2
  24. peeker.terminate(); // The preferred approach
  25. }
  26. }
  27. class Peeker extends Thread {
  28. private Blockable b;
  29. private int session;
  30. private TextField status = new TextField(40);
  31. private boolean stop = false;
  32. public Peeker(Blockable b, Container c) {
  33. c.add(status);
  34. this.b = b;
  35. start();
  36. }
  37. public void terminate() { stop = true; }
  38. public void run() {
  39. while (!stop) {
  40. status.setText(b.getClass().getName()
  41. + " Peeker " + (++session)
  42. + "; value = " + b.read());
  43. try {
  44. sleep(100);
  45. } catch (InterruptedException e){}
  46. }
  47. }
  48. } ///:Continued

Blockable类打算成为本例所有类的一个基类。一个Blockable对象包含了一个名为stateTextField(文本字段),用于显示出对象有关的信息。用于显示这些信息的方法叫作update()。我们发现它用getClass.getName()来产生类名,而不是仅仅把它打印出来;这是由于update(0)不知道自己为其调用的那个类的准确名字,因为那个类是从Blockable派生出来的。

Blockable中,变动指示符是一个int i;派生类的run()方法会为其自增。

针对每个Bloackable对象,都会启动Peeker类的一个线程。Peeker的任务是调用read()方法,检查与自己关联的Blockable对象,看看i是否发生了变化,最后用它的status文本字段报告检查结果。注意read()update()都是同步的,要求对象的锁定能自由解除,这一点非常重要。

(1) 睡眠

这个程序的第一项测试是用sleep()作出的:

  1. ///:Continuing
  2. ///////////// Blocking via sleep() ///////////
  3. class Sleeper1 extends Blockable {
  4. public Sleeper1(Container c) { super(c); }
  5. public synchronized void run() {
  6. while(true) {
  7. i++;
  8. update();
  9. try {
  10. sleep(1000);
  11. } catch (InterruptedException e){}
  12. }
  13. }
  14. }
  15. class Sleeper2 extends Blockable {
  16. public Sleeper2(Container c) { super(c); }
  17. public void run() {
  18. while(true) {
  19. change();
  20. try {
  21. sleep(1000);
  22. } catch (InterruptedException e){}
  23. }
  24. }
  25. public synchronized void change() {
  26. i++;
  27. update();
  28. }
  29. } ///:Continued

Sleeper1中,整个run()方法都是同步的。我们可看到与这个对象关联在一起的Peeker可以正常运行,直到我们启动线程为止,随后Peeker便会完全停止。这正是“堵塞”的一种形式:因为Sleeper1.run()是同步的,而且一旦线程启动,它就肯定在run()内部,方法永远不会放弃对象锁定,造成Peeker线程的堵塞。

Sleeper2通过设置不同步的运行,提供了一种解决方案。只有change()方法才是同步的,所以尽管run()位于sleep()内部,Peeker仍然能访问自己需要的同步方法——read()。在这里,我们可看到在启动了Sleeper2线程以后,Peeker会持续运行下去。

(2) 暂停和恢复

这个例子接下来的一部分引入了“挂起”或者“暂停”(Suspend)的概述。Thread类提供了一个名为suspend()的方法,可临时中止线程;以及一个名为resume()的方法,用于从暂停处开始恢复线程的执行。显然,我们可以推断出resume()是由暂停线程外部的某个线程调用的。在这种情况下,需要用到一个名为Resumer(恢复器)的独立类。演示暂停/恢复过程的每个类都有一个相关的恢复器。如下所示:

  1. ///:Continuing
  2. /////////// Blocking via suspend() ///////////
  3. class SuspendResume extends Blockable {
  4. public SuspendResume(Container c) {
  5. super(c);
  6. new Resumer(this);
  7. }
  8. }
  9. class SuspendResume1 extends SuspendResume {
  10. public SuspendResume1(Container c) { super(c);}
  11. public synchronized void run() {
  12. while(true) {
  13. i++;
  14. update();
  15. suspend(); // Deprecated in Java 1.2
  16. }
  17. }
  18. }
  19. class SuspendResume2 extends SuspendResume {
  20. public SuspendResume2(Container c) { super(c);}
  21. public void run() {
  22. while(true) {
  23. change();
  24. suspend(); // Deprecated in Java 1.2
  25. }
  26. }
  27. public synchronized void change() {
  28. i++;
  29. update();
  30. }
  31. }
  32. class Resumer extends Thread {
  33. private SuspendResume sr;
  34. public Resumer(SuspendResume sr) {
  35. this.sr = sr;
  36. start();
  37. }
  38. public void run() {
  39. while(true) {
  40. try {
  41. sleep(1000);
  42. } catch (InterruptedException e){}
  43. sr.resume(); // Deprecated in Java 1.2
  44. }
  45. }
  46. } ///:Continued

SuspendResume1也提供了一个同步的run()方法。同样地,当我们启动这个线程以后,就会发现与它关联的Peeker进入“堵塞”状态,等候对象锁被释放,但那永远不会发生。和往常一样,这个问题在SuspendResume2里得到了解决,它并不同步整个run()方法,而是采用了一个单独的同步change()方法。

对于Java 1.2,大家应注意suspend()resume()已获得强烈反对,因为suspend()包含了对象锁,所以极易出现“死锁”现象。换言之,很容易就会看到许多被锁住的对象在傻乎乎地等待对方。这会造成整个应用程序的“凝固”。尽管在一些老程序中还能看到它们的踪迹,但在你写自己的程序时,无论如何都应避免。本章稍后就会讲述正确的方案是什么。

(3) 等待和通知

通过前两个例子的实践,我们知道无论sleep()还是suspend()都不会在自己被调用的时候解除锁定。需要用到对象锁时,请务必注意这个问题。在另一方面,wait()方法在被调用时却会解除锁定,这意味着可在执行wait()期间调用线程对象中的其他同步方法。但在接着的两个类中,我们看到run()方法都是“同步”的。在wait()期间,Peeker仍然拥有对同步方法的完全访问权限。这是由于wait()在挂起内部调用的方法时,会解除对象的锁定。

我们也可以看到wait()的两种形式。第一种形式采用一个以毫秒为单位的参数,它具有与sleep()中相同的含义:暂停这一段规定时间。区别在于在wait()中,对象锁已被解除,而且能够自由地退出wait(),因为一个notify()可强行使时间流逝。

第二种形式不采用任何参数,这意味着wait()会持续执行,直到notify()介入为止。而且在一段时间以后,不会自行中止。

wait()notify()比较特别的一个地方是这两个方法都属于基类Object的一部分,不象sleep()suspend()以及resume()那样属于Thread的一部分。尽管这表面看有点儿奇怪——居然让专门进行线程处理的东西成为通用基类的一部分——但仔细想想又会释然,因为它们操纵的对象锁也属于每个对象的一部分。因此,我们可将一个wait()置入任何同步方法内部,无论在那个类里是否准备进行涉及线程的处理。事实上,我们能调用wait()的唯一地方是在一个同步的方法或代码块内部。若在一个不同步的方法内调用wait()或者notify(),尽管程序仍然会编译,但在运行它的时候,就会得到一个IllegalMonitorStateException(非法监视器状态异常),而且会出现多少有点莫名其妙的一条消息:current thread not owner(当前线程不是所有人”。注意sleep()suspend()以及resume()都能在不同步的方法内调用,因为它们不需要对锁定进行操作。

只能为自己的锁定调用wait()notify()。同样地,仍然可以编译那些试图使用错误锁定的代码,但和往常一样会产生同样的IllegalMonitorStateException异常。我们没办法用其他人的对象锁来愚弄系统,但可要求另一个对象执行相应的操作,对它自己的锁进行操作。所以一种做法是创建一个同步方法,令其为自己的对象调用notify()。但在Notifier中,我们会看到一个同步方法内部的notify()

  1. synchronized(wn2) {
  2. wn2.notify();
  3. }

其中,wn2是类型为WaitNotify2的对象。尽管并不属于WaitNotify2的一部分,这个方法仍然获得了wn2对象的锁定。在这个时候,它为wn2调用notify()是合法的,不会得到IllegalMonitorStateException异常。

  1. ///:Continuing
  2. /////////// Blocking via wait() ///////////
  3. class WaitNotify1 extends Blockable {
  4. public WaitNotify1(Container c) { super(c); }
  5. public synchronized void run() {
  6. while(true) {
  7. i++;
  8. update();
  9. try {
  10. wait(1000);
  11. } catch (InterruptedException e){}
  12. }
  13. }
  14. }
  15. class WaitNotify2 extends Blockable {
  16. public WaitNotify2(Container c) {
  17. super(c);
  18. new Notifier(this);
  19. }
  20. public synchronized void run() {
  21. while(true) {
  22. i++;
  23. update();
  24. try {
  25. wait();
  26. } catch (InterruptedException e){}
  27. }
  28. }
  29. }
  30. class Notifier extends Thread {
  31. private WaitNotify2 wn2;
  32. public Notifier(WaitNotify2 wn2) {
  33. this.wn2 = wn2;
  34. start();
  35. }
  36. public void run() {
  37. while(true) {
  38. try {
  39. sleep(2000);
  40. } catch (InterruptedException e){}
  41. synchronized(wn2) {
  42. wn2.notify();
  43. }
  44. }
  45. }
  46. } ///:Continued

若必须等候其他某些条件(从线程外部加以控制)发生变化,同时又不想在线程内一直傻乎乎地等下去,一般就需要用到wait()wait()允许我们将线程置入“睡眠”状态,同时又“积极”地等待条件发生改变。而且只有在一个notify()notifyAll()发生变化的时候,线程才会被唤醒,并检查条件是否有变。因此,我们认为它提供了在线程间进行同步的一种手段。

(4) IO堵塞

若一个数据流必须等候一些IO活动,便会自动进入“堵塞”状态。在本例下面列出的部分中,有两个类协同通用的Reader以及Writer对象工作(使用Java 1.1的流)。但在测试模型中,会设置一个管道化的数据流,使两个线程相互间能安全地传递数据(这正是使用管道流的目的)。

Sender将数据置入Writer,并“睡眠”随机长短的时间。然而,Receiver本身并没有包括sleep()suspend()或者wait()方法。但在执行read()的时候,如果没有数据存在,它会自动进入“堵塞”状态。如下所示:

  1. ///:Continuing
  2. class Sender extends Blockable { // send
  3. private Writer out;
  4. public Sender(Container c, Writer out) {
  5. super(c);
  6. this.out = out;
  7. }
  8. public void run() {
  9. while(true) {
  10. for(char c = 'A'; c <= 'z'; c++) {
  11. try {
  12. i++;
  13. out.write(c);
  14. state.setText("Sender sent: "
  15. + (char)c);
  16. sleep((int)(3000 * Math.random()));
  17. } catch (InterruptedException e){}
  18. catch (IOException e) {}
  19. }
  20. }
  21. }
  22. }
  23. class Receiver extends Blockable {
  24. private Reader in;
  25. public Receiver(Container c, Reader in) {
  26. super(c);
  27. this.in = in;
  28. }
  29. public void run() {
  30. try {
  31. while(true) {
  32. i++; // Show peeker it's alive
  33. // Blocks until characters are there:
  34. state.setText("Receiver read: "
  35. + (char)in.read());
  36. }
  37. } catch(IOException e) { e.printStackTrace();}
  38. }
  39. } ///:Continued

这两个类也将信息送入自己的state字段,并修改i值,使Peeker知道线程仍在运行。

(5) 测试

令人惊讶的是,主要的程序片(Applet)类非常简单,这是大多数工作都已置入Blockable框架的缘故。大概地说,我们创建了一个由Blockable对象构成的数组。而且由于每个对象都是一个线程,所以在按下"start"按钮后,它们会采取自己的行动。还有另一个按钮和actionPerformed()从句,用于中止所有Peeker对象。由于Java 1.2“反对”使用Threadstop()方法,所以可考虑采用这种折衷形式的中止方式。

为了在SenderReceiver之间建立一个连接,我们创建了一个PipedWriter和一个PipedReader。注意PipedReader in必须通过一个构造器参数同PipedWriterout连接起来。在那以后,我们在out内放进去的所有东西都可从in中提取出来——似乎那些东西是通过一个“管道”传输过去的。随后将inout对象分别传递给ReceiverSender构造器;后者将它们当作任意类型的ReaderWriter看待(也就是说,它们被“上溯”转换了)。

Blockable引用b的数组在定义之初并未得到初始化,因为管道化的数据流是不可在定义前设置好的(对try块的需要将成为障碍):

  1. ///:Continuing
  2. /////////// Testing Everything ///////////
  3. public class Blocking extends Applet {
  4. private Button
  5. start = new Button("Start"),
  6. stopPeekers = new Button("Stop Peekers");
  7. private boolean started = false;
  8. private Blockable[] b;
  9. private PipedWriter out;
  10. private PipedReader in;
  11. public void init() {
  12. out = new PipedWriter();
  13. try {
  14. in = new PipedReader(out);
  15. } catch(IOException e) {}
  16. b = new Blockable[] {
  17. new Sleeper1(this),
  18. new Sleeper2(this),
  19. new SuspendResume1(this),
  20. new SuspendResume2(this),
  21. new WaitNotify1(this),
  22. new WaitNotify2(this),
  23. new Sender(this, out),
  24. new Receiver(this, in)
  25. };
  26. start.addActionListener(new StartL());
  27. add(start);
  28. stopPeekers.addActionListener(
  29. new StopPeekersL());
  30. add(stopPeekers);
  31. }
  32. class StartL implements ActionListener {
  33. public void actionPerformed(ActionEvent e) {
  34. if(!started) {
  35. started = true;
  36. for(int i = 0; i < b.length; i++)
  37. b[i].start();
  38. }
  39. }
  40. }
  41. class StopPeekersL implements ActionListener {
  42. public void actionPerformed(ActionEvent e) {
  43. // Demonstration of the preferred
  44. // alternative to Thread.stop():
  45. for(int i = 0; i < b.length; i++)
  46. b[i].stopPeeker();
  47. }
  48. }
  49. public static void main(String[] args) {
  50. Blocking applet = new Blocking();
  51. Frame aFrame = new Frame("Blocking");
  52. aFrame.addWindowListener(
  53. new WindowAdapter() {
  54. public void windowClosing(WindowEvent e) {
  55. System.exit(0);
  56. }
  57. });
  58. aFrame.add(applet, BorderLayout.CENTER);
  59. aFrame.setSize(350,550);
  60. applet.init();
  61. applet.start();
  62. aFrame.setVisible(true);
  63. }
  64. } ///:~

init()中,注意循环会遍历整个数组,并为页添加statepeeker.status文本字段。

首次创建好Blockable线程以后,每个这样的线程都会自动创建并启动自己的Peeker。所以我们会看到各个Peeker都在Blockable线程启动之前运行起来。这一点非常重要,因为在Blockable线程启动的时候,部分Peeker会被堵塞,并停止运行。弄懂这一点,将有助于我们加深对“堵塞”这一概念的认识。

14.3.2 死锁

由于线程可能进入堵塞状态,而且由于对象可能拥有“同步”方法——除非同步锁定被解除,否则线程不能访问那个对象——所以一个线程完全可能等候另一个对象,而另一个对象又在等候下一个对象,以此类推。这个“等候”链最可怕的情形就是进入封闭状态——最后那个对象等候的是第一个对象!此时,所有线程都会陷入无休止的相互等待状态,大家都动弹不得。我们将这种情况称为“死锁”。尽管这种情况并非经常出现,但一旦碰到,程序的调试将变得异常艰难。

就语言本身来说,尚未直接提供防止死锁的帮助措施,需要我们通过谨慎的设计来避免。如果有谁需要调试一个死锁的程序,他是没有任何窍门可用的。

(1) Java 1.2对stop()suspend()resume()以及destroy()的反对

为减少出现死锁的可能,Java 1.2作出的一项贡献是“反对”使用Threadstop()suspend()resume()以及destroy()方法。

之所以反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态(“被析构”),那么其他线程能在那种状态下检查和修改它们。结果便造成了一种微妙的局面,我们很难检查出真正的问题所在。所以应尽量避免使用stop(),应该采用Blocking.java那样的方法,用一个标志告诉线程什么时候通过退出自己的run()方法来中止自己的执行。

如果一个线程被堵塞,比如在它等候输入的时候,那么一般都不能象在Blocking.java中那样轮询一个标志。但在这些情况下,我们仍然不该使用stop(),而应换用由Thread提供的interrupt()方法,以便中止并退出堵塞的代码。

  1. //: Interrupt.java
  2. // The alternative approach to using stop()
  3. // when a thread is blocked
  4. import java.awt.*;
  5. import java.awt.event.*;
  6. import java.applet.*;
  7. class Blocked extends Thread {
  8. public synchronized void run() {
  9. try {
  10. wait(); // Blocks
  11. } catch(InterruptedException e) {
  12. System.out.println("InterruptedException");
  13. }
  14. System.out.println("Exiting run()");
  15. }
  16. }
  17. public class Interrupt extends Applet {
  18. private Button
  19. interrupt = new Button("Interrupt");
  20. private Blocked blocked = new Blocked();
  21. public void init() {
  22. add(interrupt);
  23. interrupt.addActionListener(
  24. new ActionListener() {
  25. public
  26. void actionPerformed(ActionEvent e) {
  27. System.out.println("Button pressed");
  28. if(blocked == null) return;
  29. Thread remove = blocked;
  30. blocked = null; // to release it
  31. remove.interrupt();
  32. }
  33. });
  34. blocked.start();
  35. }
  36. public static void main(String[] args) {
  37. Interrupt applet = new Interrupt();
  38. Frame aFrame = new Frame("Interrupt");
  39. aFrame.addWindowListener(
  40. new WindowAdapter() {
  41. public void windowClosing(WindowEvent e) {
  42. System.exit(0);
  43. }
  44. });
  45. aFrame.add(applet, BorderLayout.CENTER);
  46. aFrame.setSize(200,100);
  47. applet.init();
  48. applet.start();
  49. aFrame.setVisible(true);
  50. }
  51. } ///:~

Blocked.run()内部的wait()会产生堵塞的线程。当我们按下按钮以后,blocked(堵塞)的引用就会设为null,使垃圾收集器能够将其清除,然后调用对象的interrupt()方法。如果是首次按下按钮,我们会看到线程正常退出。但在没有可供“杀死”的线程以后,看到的便只是按钮被按下而已。

suspend()resume()方法天生容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成令人难堪的死锁。所以我们不应该使用suspend()resume(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。我们可以修改前面的Counter2.java来实际体验一番。尽管两个版本的效果是差不多的,但大家会注意到代码的组织结构发生了很大的变化——为所有“听众”都使用了匿名的内部类,而且Thread是一个内部类。这使得程序的编写稍微方便一些,因为它取消了Counter2.java中一些额外的记录工作。

  1. //: Suspend.java
  2. // The alternative approach to using suspend()
  3. // and resume(), which have been deprecated
  4. // in Java 1.2.
  5. import java.awt.*;
  6. import java.awt.event.*;
  7. import java.applet.*;
  8. public class Suspend extends Applet {
  9. private TextField t = new TextField(10);
  10. private Button
  11. suspend = new Button("Suspend"),
  12. resume = new Button("Resume");
  13. class Suspendable extends Thread {
  14. private int count = 0;
  15. private boolean suspended = false;
  16. public Suspendable() { start(); }
  17. public void fauxSuspend() {
  18. suspended = true;
  19. }
  20. public synchronized void fauxResume() {
  21. suspended = false;
  22. notify();
  23. }
  24. public void run() {
  25. while (true) {
  26. try {
  27. sleep(100);
  28. synchronized(this) {
  29. while(suspended)
  30. wait();
  31. }
  32. } catch (InterruptedException e){}
  33. t.setText(Integer.toString(count++));
  34. }
  35. }
  36. }
  37. private Suspendable ss = new Suspendable();
  38. public void init() {
  39. add(t);
  40. suspend.addActionListener(
  41. new ActionListener() {
  42. public
  43. void actionPerformed(ActionEvent e) {
  44. ss.fauxSuspend();
  45. }
  46. });
  47. add(suspend);
  48. resume.addActionListener(
  49. new ActionListener() {
  50. public
  51. void actionPerformed(ActionEvent e) {
  52. ss.fauxResume();
  53. }
  54. });
  55. add(resume);
  56. }
  57. public static void main(String[] args) {
  58. Suspend applet = new Suspend();
  59. Frame aFrame = new Frame("Suspend");
  60. aFrame.addWindowListener(
  61. new WindowAdapter() {
  62. public void windowClosing(WindowEvent e){
  63. System.exit(0);
  64. }
  65. });
  66. aFrame.add(applet, BorderLayout.CENTER);
  67. aFrame.setSize(300,100);
  68. applet.init();
  69. applet.start();
  70. aFrame.setVisible(true);
  71. }
  72. } ///:~

Suspendable中的suspended(已挂起)标志用于开关“挂起”或者“暂停”状态。为挂起一个线程,只需调用fauxSuspend()将标志设为true(真)即可。对标志状态的侦测是在run()内进行的。就象本章早些时候提到的那样,wait()必须设为“同步”(synchronized),使其能够使用对象锁。在fauxResume()中,suspended标志被设为false(假),并调用notify()——由于这会在一个“同步”从句中唤醒wait(),所以fauxResume()方法也必须同步,使其能在调用notify()之前取得对象锁(这样一来,对象锁可由要唤醍的那个wait()使用)。如果遵照本程序展示的样式,可以避免使用wait()notify()

Threaddestroy()方法根本没有实现;它类似一个根本不能恢复的suspend(),所以会发生与suspend()一样的死锁问题。然而,这一方法没有得到明确的“反对”,也许会在Java以后的版本(1.2版以后)实现,用于一些可以承受死锁危险的特殊场合。

大家可能会奇怪当初为什么要实现这些现在又被“反对”的方法。之所以会出现这种情况,大概是由于Sun公司主要让技术人员来决定对语言的改动,而不是那些市场销售人员。通常,技术人员比搞销售的更能理解语言的实质。当初犯下了错误以后,也能较为理智地正视它们。这意味着Java能够继续进步,即便这使Java程序员多少感到有些不便。就我自己来说,宁愿面对这些不便之处,也不愿看到语言停滞不前。